import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:lib_uikit/lib_uikit.dart';
import 'dart:math' as math;
import '../gt_button_position.dart';
import '../shape/gt_button_shape.dart';
import '../size/gt_button_size.dart';
import '../type/gt_button_type.dart';

class GTButton extends StatefulWidget {
  const GTButton({
    Key? key,
    this.text,
    this.textColor,
    this.disabledColor,
    this.disabledTextColor,
    this.onPressed,
    this.type = GTButtonType.normal,
    this.shape = GTButtonShape.standard,
    this.size = GTButtonSize.MEDIUM,
    this.color,
    this.borderSide,
    this.focusNode,
    this.borderShape,
    this.textStyle,
    this.onHighlightChanged,
    this.autofocus = false,
    this.elevation = 0.0,
    this.padding,
    this.position = GTButtonPosition.start,
    MaterialTapTargetSize? materialTapTargetSize,
    this.enableFeedback,
    this.highlightColor,
    this.blockButton,
    this.fullWidthButton,
    this.child,
    this.icon,
    this.buttonWidth,
    this.buttonHeight,
  })  : materialTapTargetSize =
            materialTapTargetSize ?? MaterialTapTargetSize.padded,
        super(key: key);

  final VoidCallback? onPressed;

  final ValueChanged<bool>? onHighlightChanged;

  /// Pass [GTColors] or [Color]
  final Color? color;
  final double size;

  /// Pass [GTColors] or [Color]
  final Color? textColor;
  final Color? disabledColor;
  final Color? disabledTextColor;
  final Color? highlightColor;
  final Widget? icon;
  final GTButtonType type;
  final bool autofocus;
  final bool? enableFeedback;
  final bool? blockButton;
  final bool? fullWidthButton;
  final String? text;
  final double elevation;
  final double? buttonWidth;
  final double? buttonHeight;

  final Widget? child;

  /// The border side for the button's [Material].
  final BorderSide? borderSide;
  final GTButtonShape shape;
  final ShapeBorder? borderShape;

  final MaterialTapTargetSize materialTapTargetSize;

  final TextStyle? textStyle;
  final FocusNode? focusNode;
  final EdgeInsetsGeometry? padding;
  final GTButtonPosition position;

  bool get enabled => onPressed != null;

  @override
  State<StatefulWidget> createState() => _GTButtonState();
}

class _GTButtonState extends State<GTButton> {
  late Color color;
  Color? textColor;
  Color? disabledColor;
  Color? disabledTextColor;
  Widget? icon;
  Widget? child;
  Function? onPressed;
  late GTButtonType type;
  late GTButtonShape shape;
  late GTButtonPosition position;
  double? buttonWidth;
  double? buttonHeight;

  late double size;
  final Set<MaterialState> _states = <MaterialState>{};

  @override
  void initState() {
    buttonWidth = widget.buttonWidth;
    buttonHeight = widget.buttonHeight;
    color = widget.color ?? GTColors.uikitBrand1.color;
    textColor = widget.textColor;
    disabledColor = widget.disabledColor;
    disabledTextColor = widget.disabledTextColor;
    child = widget.text != null ? Text(widget.text!) : widget.child;
    textColor = widget.textColor;
    child = widget.text != null ? Text(widget.text!) : widget.child;
    icon = widget.icon;
    onPressed = widget.onPressed;
    type = widget.type;
    shape = widget.shape;
    size = widget.size;
    position = widget.position;
    disabledColor = widget.disabledColor;
    disabledTextColor = widget.disabledTextColor;
    _updateState(
      MaterialState.disabled,
      !widget.enabled,
    );
    super.initState();
  }

  @override
  void didUpdateWidget(GTButton oldWidget) {
    _updateState(MaterialState.disabled, !widget.enabled);
    // If the button is disabled while a press gesture is currently ongoing,
    // InkWell makes a call to handleHighlightChanged. This causes an exception
    // because it calls setState in the middle of a build. To preempt this, we
    // manually update pressed to false when this situation occurs.
    buttonWidth = widget.buttonWidth;
    buttonHeight = widget.buttonHeight;
    color = widget.color ?? GTColors.uikitBrand1.color;
    textColor = widget.textColor;
    child = widget.text != null ? Text(widget.text!) : widget.child;
    icon = widget.icon;
    onPressed = widget.onPressed;
    type = widget.type;
    shape = widget.shape;
    size = widget.size;
    position = widget.position;
    disabledColor = widget.disabledColor;
    disabledTextColor = widget.disabledTextColor;
    _updateState(
      MaterialState.disabled,
      !widget.enabled,
    );
    super.didUpdateWidget(oldWidget);
  }

  void _updateState(MaterialState state, bool value) {
    value ? _states.add(state) : _states.remove(state);
  }

  void _handleHoveredChanged(bool value) {
    if (_hovered != value) {
      setState(() {
        _updateState(MaterialState.hovered, value);
      });
    }
  }

  void _handleFocusedChanged(bool value) {
    if (_focused != value) {
      setState(() {
        _updateState(MaterialState.focused, value);
      });
    }
  }

  bool get _hovered => _states.contains(MaterialState.hovered);

  bool get _focused => _states.contains(MaterialState.focused);

  bool get _pressed => _states.contains(MaterialState.pressed);

  bool get _disabled => _states.contains(MaterialState.disabled);

  double? getButtonWidth() {
    if (buttonWidth != null) return buttonWidth!;
    if (widget.blockButton == true) {
      buttonWidth = MediaQuery.of(context).size.width * 0.88;
    } else if (widget.fullWidthButton == true) {
      buttonWidth = MediaQuery.of(context).size.width;
    } else {
      buttonWidth = null;
    }
    return buttonWidth;
  }

  double get _effectiveElevation {
    return widget.elevation;
  }

  @override
  Widget build(BuildContext context) {
    ShapeBorder shapeBorderType;
    Color getHighlightColor() {
      if (widget.highlightColor != null) {
        return widget.highlightColor!;
      }
      if (widget.type == GTButtonType.text ||
          widget.type == GTButtonType.stroke) {
        return Colors.transparent;
      }
      return GTColors.uikitBrand2.color;
    }

    Color getBorderColor() {
      if (widget.type != GTButtonType.stroke) return Colors.transparent;
      if (widget.enabled) {
        final Color fillColor = color;
        return fillColor;
      } else {
        if (disabledColor != null) {
          return disabledColor!;
        } else {
          return GTColors.uikitNeutral3.color;
        }
      }
    }

    Color getDisabledFillColor() {
      if (widget.type == GTButtonType.text ||
          widget.type == GTButtonType.stroke) {
        return Colors.transparent;
      }
      if (disabledColor != null) {
        return disabledColor!;
      } else {
        return GTColors.uikitNeutral3.color;
      }
    }

    Color getColor() {
      if (widget.type == GTButtonType.text ||
          widget.type == GTButtonType.stroke) {
        return Colors.transparent;
      }
      if (widget.type == GTButtonType.normal) {
        return GTColors.uikitBrand1.color;
      }
      if (widget.type == GTButtonType.second) {
        return GTColors.uikitNeutral2.color;
      }
      final Color fillColor = color;
      return fillColor;
    }

    Color getDisabledTextColor() {
      if (disabledTextColor != null) {
        return disabledTextColor!;
      } else {
        return GTColors.uikitText4.color;
      }
    }

    Color getTextColor() {
      if (textColor == null) {
        if (widget.type == GTButtonType.second ||
            widget.type == GTButtonType.stroke ||
            widget.type == GTButtonType.text) {
          return widget.enabled
              ? textColor == null
                  ? GTColors.uikitBrand1.color
                  : textColor!
              : getDisabledTextColor();
        } else {
          return GTColors.uikitText1.dark;
        }
      } else {
        return textColor!;
      }
    }

    final Color? effectiveTextColor = MaterialStateProperty.resolveAs<Color?>(
        widget.textStyle?.color, _states);

    final BorderSide outlineBorder = BorderSide(
      color: widget.borderSide == null
          ? getBorderColor()
          : widget.borderSide!.color,
      width: (widget.borderSide?.width ?? 1.0),
    );
    Size minSize;
    switch (widget.materialTapTargetSize) {
      case MaterialTapTargetSize.padded:
        minSize = const Size(48, 48);
        break;
      case MaterialTapTargetSize.shrinkWrap:
        minSize = Size.zero;
        break;
      default:
        minSize = Size.zero;
        break;
    }
    final BorderSide shapeBorder = widget.type == GTButtonType.stroke
        ? outlineBorder
        : widget.borderSide ??
            BorderSide(
              color: getBorderColor(),
              width: 0,
            );

    if (shape == GTButtonShape.pills) {
      shapeBorderType = RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(
          50,
        ),
        side: shapeBorder,
      );
    } else if (shape == GTButtonShape.square) {
      shapeBorderType = RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(0),
        side: shapeBorder,
      );
    } else if (shape == GTButtonShape.standard) {
      shapeBorderType = RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
        side: shapeBorder,
      );
    } else {
      shapeBorderType = RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(50),
        side: shapeBorder,
      );
    }

    TextStyle getTextStyle() {
      if (widget.size == GTButtonSize.SMALL) {
        return TextStyle(
          color: widget.enabled ? getTextColor() : getDisabledTextColor(),
          fontSize: 12,
          fontWeight: FontWeight.w600,
        );
      } else if (widget.size == GTButtonSize.MEDIUM) {
        return TextStyle(
          color: widget.enabled ? getTextColor() : getDisabledTextColor(),
          fontSize: 14,
          fontWeight: FontWeight.w600,
        );
      } else if (widget.size == GTButtonSize.LARGE) {
        return TextStyle(
          color: widget.enabled ? getTextColor() : getDisabledTextColor(),
          fontSize: 16,
          fontWeight: FontWeight.w600,
        );
      }
      return TextStyle(
        color: widget.enabled ? getTextColor() : getDisabledTextColor(),
        fontSize: 14,
        fontWeight: FontWeight.w400,
      );
    }

    double getIconMargin() {
      if (size == GTButtonSize.SMALL) {
        return 4;
      } else if (size == GTButtonSize.MEDIUM) {
        return 8;
      } else {
        return 8;
      }
    }

    EdgeInsetsGeometry getPadding() {
      if (widget.padding == null) {
        if (size == GTButtonSize.SMALL) {
          return const EdgeInsets.symmetric(horizontal: 8);
        } else if (size == GTButtonSize.MEDIUM) {
          return const EdgeInsets.symmetric(horizontal: 12);
        } else {
          return const EdgeInsets.symmetric(horizontal: 16);
        }
      } else {
        return widget.padding!;
      }
    }

    final Widget result = Container(
      constraints: BoxConstraints(minWidth: size, minHeight: size),
      child: Material(
        elevation: _effectiveElevation,
        textStyle: widget.textStyle ?? getTextStyle(),
        shape: widget.type == GTButtonType.text
            ? null
            : widget.borderShape ?? shapeBorderType,
        color: widget.enabled ? getColor() : getDisabledFillColor(),
        type: MaterialType.button,
        child: InkWell(
          focusNode: widget.focusNode,
          canRequestFocus: widget.enabled,
          onFocusChange: _handleFocusedChanged,
          autofocus: widget.autofocus,
          onHover: _handleHoveredChanged,
          onTap: widget.onPressed,
          enableFeedback: widget.enableFeedback,
          splashColor: Colors.transparent,
          highlightColor: getHighlightColor(),
          focusColor: Colors.transparent,
          hoverColor: Colors.transparent,
          customBorder: widget.type == GTButtonType.text
              ? null
              : widget.borderShape ?? shapeBorderType,
          child: IconTheme.merge(
            data: IconThemeData(color: effectiveTextColor),
            child: Container(
              height: buttonHeight ?? size,
              width: getButtonWidth(),
              padding: getPadding(),
              child: Center(
                widthFactor: 1,
                heightFactor: 1,
                child: icon != null &&
                        child != null &&
                        (position == GTButtonPosition.start)
                    ? Row(
                        mainAxisSize: MainAxisSize.min,
                        children: <Widget>[
                          icon!,
                          SizedBox(width: getIconMargin()),
                          child!
                        ],
                      )
                    : icon != null &&
                            child != null &&
                            (position == GTButtonPosition.end)
                        ? Row(
                            mainAxisSize: MainAxisSize.min,
                            children: <Widget>[
                              child!,
                              SizedBox(width: getIconMargin()),
                              icon!
                            ],
                          )
                        : icon ?? child,
              ),
            ),
          ),
        ),
      ),
    );

    return Semantics(
      container: true,
      button: true,
      enabled: widget.enabled,
      child: _InputPadding(
        minSize: minSize,
        child: Focus(
          focusNode: widget.focusNode,
          onFocusChange: _handleFocusedChanged,
          autofocus: widget.autofocus,
          child: result,
        ),
      ),
    );
  }
}

class _InputPadding extends SingleChildRenderObjectWidget {
  const _InputPadding({
    Key? key,
    Widget? child,
    this.minSize,
  }) : super(
          key: key,
          child: child,
        );

  final Size? minSize;

  @override
  RenderObject createRenderObject(BuildContext context) =>
      _RenderInputPadding(minSize);

  @override
  void updateRenderObject(
      BuildContext context, covariant _RenderInputPadding renderObject) {
    renderObject.minSize = minSize;
  }
}

class _RenderInputPadding extends RenderShiftedBox {
  _RenderInputPadding(this._minSize, [RenderBox? child]) : super(child);

  Size? get minSize => _minSize;
  Size? _minSize;

  set minSize(Size? value) {
    _minSize = value;
    markNeedsLayout();
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    if (child != null && minSize != null) {
      return math.max(child!.getMinIntrinsicWidth(height), minSize!.width);
    }
    return 0;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    if (child != null && minSize != null) {
      return math.max(child!.getMinIntrinsicHeight(width), minSize!.height);
    }
    return 0;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    if (child != null && minSize != null) {
      return math.max(child!.getMaxIntrinsicWidth(height), minSize!.width);
    }
    return 0;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    if (child != null && minSize != null) {
      return math.max(child!.getMaxIntrinsicHeight(width), minSize!.height);
    }
    return 0;
  }

  @override
  void performLayout() {
    if (child != null && minSize != null) {
      child!.layout(constraints, parentUsesSize: true);
      // ignore: avoid_as
      final BoxParentData childParentData = child!.parentData as BoxParentData;
      final double height = math.max(child!.size.width, minSize!.width);
      final double width = math.max(child!.size.height, minSize!.height);
      size = constraints.constrain(Size(height, width));
      childParentData.offset =
          // ignore: avoid_as
          Alignment.center.alongOffset(size - child!.size as Offset);
    } else {
      size = Size.zero;
    }
  }

  @override
  bool hitTest(BoxHitTestResult result, {required Offset position}) {
    if (super.hitTest(result, position: position)) {
      return true;
    }

    if (child != null) {
      final Offset center = child!.size.center(Offset.zero);
      return result.addWithRawTransform(
        transform: MatrixUtils.forceToPoint(center),
        position: center,
        hitTest: (BoxHitTestResult result, Offset position) {
          assert(position == center);
          return child!.hitTest(
            result,
            position: center,
          );
        },
      );
    }

    throw Exception('child property cannot be null');
  }
}
